--[[
   KahLua KonferSK - a suicide kings loot distribution addon.
     WWW: http://kahluamod.com/ksk
     SVN: http://kahluamod.com/svn/konfersk
     IRC: #KahLua on irc.freenode.net
     E-mail: cruciformer@gmail.com
   Please refer to the file LICENSE.txt for the Apache License, Version 2.0.

   Copyright 2008-2010 James Kean Johnston. All rights reserved.

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
]]

local K = LibStub:GetLibrary("KKore")
local H = LibStub:GetLibrary("KKoreHash")
local DB = LibStub:GetLibrary("KKoreDB")
local KUIBase = LibStub:GetLibrary("KKoreUI")

if (not K) then
  error ("KahLua KonferSK: could not find KahLua Kore.", 2)
end

if (not H) then
  error ("KahLua KonferSK: could not find KahLua Kore Hash library.", 2)
end

local ksk = K:GetAddon ("KKonferSK")
local L = ksk.L
local KUI = ksk.KUI
local MakeFrame = KUI.MakeFrame

-- Local aliases for global or Lua library functions
local _G = _G
local tinsert = table.insert
local tremove = table.remove
local setmetatable = setmetatable
local tconcat = table.concat
local tsort = table.sort
local tostring = tostring
local GetTime = GetTime
local min = math.min
local max = math.max
local strfmt = string.format
local strsub = string.sub
local strlen = string.len
local strfind = string.find
local strlower = string.lower
local gsub = string.gsub
local gmatch = string.gmatch
local xpcall, pcall = xpcall, pcall
local pairs, next, type = pairs, next, type
local select, assert, loadstring = select, assert, loadstring
local printf = K.printf
local strsplit = string.split
local bxor = bit.bxor

local ucolor = K.ucolor
local ecolor = K.ecolor
local icolor = K.icolor
local debug = ksk.debug
local info = ksk.info
local err = ksk.err
local white = ksk.white
local class = ksk.class
local aclass = ksk.aclass

--[[
This file contains all of the functions for dealing with sending and
receiving of inter-mod communications. It is where the KonferSK "protocol"
is implemented. A few brief notes about how things work.

Almost all events center around looting and are generated by the master
looter. Other maintenance events can be generated by any admin, and a few
are generated by normal users. Each configuration can be one of two types:
a guild configuration or a raid configuration. The main difference is
where the communication for the mod takes place. Logically enough, a guild
configuration uses the guild mod channel and a raid configuration uses the
current raid. However, even for guild configurations, some events are
raid-specific (like looting) and will be sent via that channel.

In general the person who sent the event has already done the work
described by the event. For example, when the master looter loots a
boss or chest, they will send out the list of items to populate the loot
window with. However, their own loot list will already have been populated.

Events are always sent with an event name, intended recipient (or 0 for
all) and the event data. The main KSK version is also sent with each message
so that newer clients can decode messages from older ones should the
protocol change. Each message also includes a checksum to ensure there is
no message "spoofing". In order to support multiple configurations, the
message preamble also includes the configuration ID. Please also note that all
message sends only ever happen within the current configuration, but all
messge receipts can be for any configuration and the message handlers must
therefore act accordingly.
]]

local function getbool (str)
  if (str and str == "Y") then
    return true
  end
  return false
end

local ihandlers = {}
local ehandlers = {}

local function send_addon_msg (cfg, cmd, prio, dist, target, ...)
  local cfg = cfg or ksk.currentid
  local prio = prio or "BULK"
  local fs = strfmt ("%02x:%s:%s:", ksk.protocol, cmd, cfg)
  local crc = H:CRC32 (fs, nil, false)
  local ndata = K.Serialise (...)
  crc = H:CRC32 (ndata, crc, true)
  fs = fs .. K.hexstr (crc) .. ":" .. ndata

  debug (9, "sending: cfg=%s fs=%q dist=%q target=%q prio=%q", cfg, tostring(fs:gsub("\124", "\124\124")), tostring(dist), tostring(target), tostring(prio))
  K.comm.SendCommMessage (ksk.CHAT_MSG_PREFIX, fs, dist, target, prio)
end

function ksk.SendRaidAM (cmd, prio, ...)
  if (not ksk.inraid) then
    return
  end
  send_addon_msg (ksk.currentid, cmd, prio, "RAID", nil, ...)
end

function ksk.CSendRaidAM (cfg, cmd, prio, ...)
  if (not ksk.inraid) then
    return
  end
  send_addon_msg (cfg, cmd, prio, "RAID", nil, ...)
end

function ksk.SendGuildAM (cmd, prio, ...)
  send_addon_msg (ksk.currentid, cmd, prio, "GUILD", nil, ...)
end

function ksk.CSendGuildAM (cfg, cmd, prio, ...)
  send_addon_msg (cfg, cmd, prio, "GUILD", nil, ...)
end

function ksk.CSendAM (cfg, cmd, prio, ...)
  local tp = "GUILD"
  if (ksk.configs[cfg or ksk.currentid].cfgtype == ksk.CFGTYPE_PUG) then
    if (ksk.inraid ~= true) then
      return
    end
    tp = "RAID"
  end
  send_addon_msg (cfg, cmd, prio, tp, nil, ...)
end

function ksk.SendAM (cmd, prio, ...)
  ksk.CSendAM (ksk.currentid, cmd, prio, ...)
end

function ksk.SendWhisperAM (target, cmd, prio, ...)
  send_addon_msg (ksk.currentid, cmd, prio, "WHISPER", target, ...)
end

function ksk.CSendWhisperAM (cfg, target, cmd, prio, ...)
  send_addon_msg (cfg, cmd, prio, "WHISPER", target, ...)
end

function ksk.SendRaidMsg (text)
  SendChatMessage (text, "RAID")
end

function ksk.SendRaidWarning (text)
  if (ksk.isaorl == true) then
    SendChatMessage (text, "RAID_WARNING")
  else
    SendChatMessage (text, "RAID")
  end
end

function ksk.SendGuildMsg (text)
  SendChatMessage (text, "GUILD")
end

function ksk.SendWhisper (text, target)
  SendChatMessage (text, "WHISPER", nil, target)
end

--
-- Main message receipt function. Checks the protocol, checksum and other
-- sundry stuff before invoking the handler function.
--
local oldwarn = true
local function old_warning_dialog ()
  if (not oldwarn) then
    return
  end
  oldwarn = false

  local arg = {
    name = "KSKOldProtoDialog",
    x = "CENTER", y = "MIDDLE", border = true, blackbg = true,
    okbutton = { text = K.OK_STR }, canmove = false, canresize = false,
    escclose = false, width = 450, height = 100, title = L["MODTITLE"],
  }
  local dlg = KUI:CreateDialogFrame (arg)
  dlg.OnAccept = function (this)
    this:Hide ()
  end
  dlg.OnCancel = function (this)
    this:Hide ()
  end

  arg = {
    x = 8, y = -10, width = 410, height = 64, autosize = false,
    color = { r = 1, g = 0, b = 0, a = 1},
    text = L["MODTITLE"] .. ": " .. strfmt (L["your version of %s is out of date. Please update it."], L["MODTITLE"]), font = "GameFontNormal",
    justifyv = "TOP",
  }
  dlg.str1 = KUI:CreateStringLabel (arg, dlg)

  if (ksk.mainwin and ksk.mainwin:IsShown ()) then
    ksk.mainwin:Hide ()
  end
  dlg:Show ()
end

local function commdispatch (sender, proto, cmd, cfg, res, ...)
  if (res) then
    if (not ihandlers[cmd] and not ehandlers[cmd]) then
      old_warning_dialog ()
      debug (1, "unknown command %q from %q (p=%d)", cmd, sender, proto)
      return
    else
      debug (8, "COMM(%s) received from %q (proto=%d cfgid=%s)", cmd, sender, proto, cfg)

      if (ihandlers[cmd]) then
        ihandlers[cmd] (sender, proto, cmd, cfg, ...)
      else
        if (not ksk.configs[cfg]) then
          --
          -- We ignore any events for configs we dont know about.
          --
          return
        end
        local adm = ksk.csdata[cfg].isadmin
        local estr, scrc, eid, oldeid, userevt = ...
        if ((not adm) and (not userevt)) then
          -- We're not an admin and this is an admin-only event - return.
          return
        end
        if (adm) then
          --
          -- Here we need to see if we are up to date with the sender,
          -- by checking to see if our last eventid matches their
          -- oldeid they just sent us. If it does, we process the event.
          -- If not, we ignore it because it means we are out of sync.
          --
          local senduid = ksk:FindUser (sender, cfg)
          if (not senduid) then
            --
            -- If we didn't find the user in the user lists, it means our
            -- user list and config is out of date with respect to the sender.
            -- They think they are an admin (only admins can send events)
            -- but we don't have them even as a simple user, let alone an
            -- admin. So we have to ignore this event. At some point we will
            -- need to sync with the config owner, and get the users list and
            -- config setting that has this user marked as an admin. Until
            -- then all we can do is ignore the event.
            --
            return
          end

          --
          -- When we check to see if we are an admin, the call checks to
          -- see if the sender is an alt, and that the alt's main is the
          -- owner or and admin. It always returns the real ID of the
          -- user's main if this user is an alt. We capture that user ID to
          -- use for checking the sender's credentials by ID, as that is
          -- the actual user we care about.
          --
          local ia, cksenduid = ksk:IsAdmin (senduid, cfg)
          if (not cksenduid) then
            cksenduid = senduid
          end
          if (not ia) then
            --
            -- Likewise for admins. We do not have this user marked as an
            -- admin in our lists, so it means we are out of sync with at
            -- least the config owner. Since we don't know for sure the user
            -- is an admin, we simply return and we can get this event later
            -- when we sync.
            --
            return
          end

          --
          -- If we are an admin and we havent started a sync relationship
          -- with anyone yet, we can still process the event below (it does
          -- no harm and will keep their config as up to date as possible).
          -- There is no real benefit to doing this except that we can, and
          -- an unsynced admin will still have a rough idea of what's what
          -- with the configuration. However, if we are syncing, we need to
          -- check to see if the last event we received from this user was
          -- the same as we have recorded for them, and if so, process the
          -- event as per normal. If not, we simply return as it means we
          -- are out of sync with respects to the sender, and we need to do
          -- a sync.
          --
          if (ksk.configs[cfg].syncing) then
            if (ksk.configs[cfg].admins[cksenduid].lastevent ~= oldeid) then
              --
              -- The last event we have for them is not their last event, we
              -- are out of sync with them. Return now.
              --
              return
            end

            --
            -- If we get down here it means we are a syncer, the sender is a
            -- syncer, and we are up to date with respects to each other. We
            -- need to update the config checksum with this new event, and
            -- update the last event received from this sender.
            --
            local oldsum = ksk.configs[cfg].cksum
            local newsum = bxor (oldsum, tonumber (scrc))
            ksk.configs[cfg].cksum = newsum
            ksk.configs[cfg].admins[cksenduid].lastevent = eid
            ksk.qf.synctopbar:SetCurrentCRC ()

            --
            -- Since this is a directly handled event, we want a convenient
            -- way of telling the receiving function this. It may effect
            -- whether or not it does a refresh (for example if we are
            -- processing a slew of events we will do a refresh at the end).
            -- So, we multiply the adm value by 10 to indicate it is
            -- immediate.
            --
            adm = adm * 10
          end
        end
        ehandlers[cmd] (adm, sender, proto, cmd, cfg, strsplit (":", estr))
      end
    end
  else
    debug (1, "failed to deserialise %q from %q (p=%d)", sender, cmd, proto)
  end
end

local userwarn = userwarn or {}

function ksk:OnCommReceived (prefix, msg, dist, sender)
  if (sender == K.player.player) then
    return -- Ignore our own messages
  end
  if (dist == "UNKNOWN" and (sender ~= nil and sender ~= "")) then
    return
  end

  debug (9, "received: prefix=%q msg=%q dist=%q sender=%q", tostring(prefix), tostring(msg), tostring(dist), tostring(sender))

  local iter = gmatch (msg, "([^:]+)()")
  local ps = iter()
  if (not ps) then
    debug (1, "bad msg %q received from %q", msg, sender)
    return
  end
  local proto = tonumber (ps, 16)
  if (proto > ksk.protocol) then
    old_warning_dialog ()
    return
  end

  local cmd = iter ()
  if (not cmd) then
    debug (1, "malformed msg %q received from %q", msg, sender)
    return
  end
  local cfg = iter ()
  if (not cfg) then
    debug (1, "malformed msg %q received from %q", msg, sender)
    return
  end
  local msum, pos = iter ()
  if (not msum) then
    debug (1, "malformed msg %q received from %q", msg, sender)
    return
  end
  local data = string.sub (msg, pos+1)
  if (not data) then
    debug (1, "malformed msg %q received from %q", msg, sender)
    return
  end
  local crc = H:CRC32 (ps, nil, false)
  crc = H:CRC32 (":", crc, false)
  crc = H:CRC32 (cmd, crc, false)
  crc = H:CRC32 (":", crc, false)
  crc = H:CRC32 (cfg, crc, false)
  crc = H:CRC32 (":", crc, false)
  crc = H:CRC32 (data, crc, true)
  local mf = K.hexstr (crc)

  if (mf ~= msum) then
    if (not userwarn[sender]) then
      printf (ecolor, "WARNING: addon message from %q was fake!", sender)
      userwarn[sender] = true
    end
  end
  commdispatch (sender, proto, cmd, cfg, K.Deserialise (data))
end

--
-- Command: DEBUG level message ...
-- Purpose: Displays a debug message from the given sender
--
ihandlers.DEBUG = function (sender, proto, cmd, cfg, ...)
  local lev = ...
  debug (lev, "<-" .. sender .. "->: " .. select (1, ...))
end

--
-- Command: VCHEK
-- Purpose: Respond to sender with a version check
--
ihandlers.VCHEK = function (sender, proto, cmd, cfg, ...)
  ksk.SendWhisperAM (sender, "VCACK", nil, ksk.version)
end

--
-- Command: VCACK version
-- Purpose: Sent back to us in response to a VCHEK command
--
ihandlers.VCACK = function (sender, proto, cmd, cfg, ver)
  if (not ksk.vcreplies[sender]) then
    ksk.vcreplies[sender] = true
    local ts = ""
    if (ksk.inraid) then
      if (ksk.raid and ksk.raid.players and ksk.raid.players[sender]) then
        ts = " " .. L["(in raid)"]
      end
    end
    info (L["%s using version %s"] .. ts, white (sender), white (ver))
  end
end

--
-- Command: OLOOT uname uguid realguid lootidtable
-- Purpose: Sent when the master looter loots a corpse or chest. This is sent
--          each time they do so, whether its for the same mob/chest or not.
--          So, if the ML clicks away from a mob or closes loot due to combat
--          or whatever, a loot close event will be sent, and when they
--          reloot the mob/chest, this event will be sent again. It is always
--          a table with the full list of unresolved loot. Each element in the
--          table is the itemlinka nd quantity for the item in question.
--          The uname parameter is the name of the mob being looted, and the
--          uguid is the GUID of the unit being looted. This is set to the name
--          of the chest or container being opened if its not a real mob with
--          a GUID. In this case, realguid will be false, for real mobs with
--          GUID's it will be true.
--
local we_opened = nil

ihandlers.OLOOT = function (sender, proto, cmd, cfg, ...)
  local uname, uguid, realguid, loottbl = ...
  if (cfg ~= ksk.currentid) then
    return
  end

  ksk:ResetBossLoot ()
  ksk.bossloot = {}

  for k,v in ipairs (loottbl) do
    local ilink, quant = unpack(v)
    local itemid = string.match (ilink, "item:(%d+)")
    local ti = { itemid = itemid, ilink = ilink, slot = 0, quant = quant }
    tinsert (ksk.bossloot, ti)
  end

  ksk.qf.lootscroll.itemcount = #ksk.bossloot
  ksk.qf.lootscroll:UpdateList ()

  ksk.autolooted = ksk.autolooted or {}
  if (ksk.autolooted[uguid] == true) then
    return
  end
  if (uguid ~= "0") then
    ksk.autolooted[uguid] = true
  end

  if (ksk.suspended or not ksk.configs[cfg].settings.auto_bid) then
    return
  end

  if (not ksk.mainwin:IsShown ()) then
    ksk.mainwin:Show ()
    ksk.mainwin:SetTab (ksk.LOOT_TAB, ksk.LOOT_ASSIGN_TAB)
    we_opened = true
  end
end

--
-- Command: CLOOT
-- Purpose: Sent when the master looter stops looting a corpse or chest.
--          Close the main window when we receive this if we opened it during
--          OLOOT processing.
--
ihandlers.CLOOT = function (sender, proto, cmd, cfg, ...)
  ksk:ResetBossLoot ()
  if (we_opened) then
    we_opened = nil
    ksk.mainwin:Hide ()
  end
end

--
-- Command: ALOOT itemlink
-- Purpose: Sent when the master looter adds an item to the loot list
--          manually. ITEMLINK is the item link of the item being added
--
ihandlers.ALOOT = function (sender, proto, cmd, cfg, ilink)
  if (cfg ~= ksk.currentid) then
    return
  end
  ksk:AddLoot (ilink, true)
end

--
-- Command: BIDCL itemid
-- Purpose: Clear the current bid list. This is usually sent when the item
--          being bidded on changes in the middle of a bid. This is not the
--          same thing as a bid closing successfully. This completely clears
--          the bid list. Although it is currently unused, this event
--          receives the itemid of the current item being bid on.
--
ihandlers.BIDCL = function (sender, proto, cmd, cfg, ...)
  local itemid = ...
  if (cfg ~= ksk.currentid) then
    return
  end
  if (not ksk.bossloot) then
    return
  end
  ksk:ResetBidders (false)
end

--
-- Command: LISEL idx itemid filter role list rank
-- Purpose: Select loot item at index position IDX. The item should have
--          item id of ITEMID. FILTER is the class filter string, ROLE is the
--          user role filter, and LIST is the list ID of the list that it
--          will be rolled on (unless the master looter changes the list).
--          RANK is the default rank filter value that can be set either by
--          the list being rolled on or by the item options.
--
ihandlers.LISEL = function (sender, proto, cmd, cfg, ...)
  local idx, itemid, filter, role, list, rank = ...
  if (cfg ~= ksk.currentid) then
    return
  end
  if (not ksk.bossloot) then
    return
  end
  ksk:SelectLootItem (idx, itemid, filter, role, list, rank)
end

--
-- Command: LLSEL listid clearbids
-- Purpose: Select the specified listid as the current loot list. This is sent
--          when the master looter explicitly changes lists, but not  when
--          an item is selected that has a specific roll list associated with
--          it. That is taken care of by LISEL above. This is typically
--          preceeded by a bidder clear event (BIDCL). If this function
--          itself should clear bids, then CLEARBIDS is set to true.
--
ihandlers.LLSEL = function (sender, proto, cmd, cfg, ...)
  local listid, clearbids = ...
  if (cfg ~= ksk.currentid) then
    return
  end
  if (not ksk.bossloot) then
    return
  end
  if (clearbids) then
    ksk:ResetBidList ()
  end
  ksk:SelectLootListByID (listid)
end

local function prepare_config_from_bcast (cfd)
  local ncf = {}

  local cfgid, cfgname, cfgtype, tethered, uloot, owner, ts, crc
  if (cfd.v == 1) then
    cfgid, cfgname, cfgtype, tethered, uloot, owner, crc = strsplit (":", cfd.c)
    ncf.name = cfgname
    ncf.cfgtype = tonumber (cfgtype)
    ncf.tethered = getbool (tethered)
    ncf.owner = owner
    ncf.cksum = tonumber (crc)
    ncf.lastevent = 0

    ncf.history = {}

    ncf.nusers = cfd.u[1]
    ncf.users = {}
    for k,v in pairs (cfd.u[2]) do
      local uid, uname, class, role, flags, isalt, alts = strsplit (":", v)
      isalt = getbool (isalt)
      ncf.users[uid] = { name = uname, class = class, role = tonumber(role),
        flags = flags }
      if (isalt) then
        ncf.users[uid].main = alts
      elseif (alts ~= "0") then
        ncf.users[uid].alts = ksk:SplitRaidList (alts)
      end
    end

    ncf.nadmins = cfd.a[1]
    ncf.admins = {}
    for k,v in pairs (cfd.a[2]) do
      local au, ai = strsplit (":", v)
      ncf.admins[au] = { id = ai }
    end

    ncf.nlists = cfd.l[1]
    ncf.lists = {}
    for k,v in pairs (cfd.l[2]) do
      local lid, lname, sorder, drank, sc, sr, el, numu, ulist = strsplit (":", v)
      ncf.lists[lid] = { name = lname, sortorder = tonumber (sorder),
        def_rank = tonumber (drank), strictcfilter = getbool (sc),
        strictrfilter = getbool (sr), extralist = el, nusers = tonumber (numu),
        users = ksk:SplitRaidList (ulist) }
    end

    if (cfd.s) then
      for k,v in pairs (cfd.s) do
        local adm, le = strsplit (":", v)
        ncf.admins[adm].lastevent = tonumber (le)
      end
    end

    if (cfd.i) then
      ncf.nitems = cfd.i[1]
      ncf.items = {}
      K.CopyTable  (cfd.i[2], ncf.items)
    end

    ncf.settings = {}
    K.CopyTable (ksk.defaults, ncf.settings)
    if (cfd.d and cfd.e) then
      ncf.syncing = true
      ncf.admins[cfd.d].lastevent = cfd.e
      ncf.admins[cfd.d].active = true
    end
  end

  return ncf, cfgid
end

--
-- Command: BCAST configdata
-- Purpose: Sent by a guild master or officer with suitable guild permissions
--          to broadcast the entire current configuration to users who are
--          not actively syncing co-admins. This message includes the full
--          user list, all roll lists, and the full members list for each
--          roll list. About the only thing it does not include is the item
--          database. Only syncers care about that, and they get it via a
--          full sync. If this is a PUG config, we pop up a confirmation
--          window if the user did not have that config before, as they
--          may not care to have some random PUG's config data.
--
ihandlers.BCAST = function (sender, proto, cmd, cfg, cfd)
  local ncf, cfgid = prepare_config_from_bcast (cfd)
  local cfgtype = ncf.cfgtype

  --
  -- First step is to verify that the sending user is someone we should be
  -- paying attention to. If the configuration is a guild configuration,
  -- the user needs to be the GM or an officer (someone who can read
  -- officer chat). If it is a PUG configuration, the person needs to be
  -- the master looter or a raid admin. If any of those
  -- conditions is not true, silently ignore the message.
  --
  if (K.player.isguilded and cfgtype == ksk.CFGTYPE_GUILD) then
    -- Guild config
    if (not UnitIsInMyGuild (sender)) then
      local ugn = GetGuildInfo (sender)
      if (not ugn) then
        ugn = "-"
      end
      if (ugn ~= K.player.guild) then
        debug (2, "BCAST returning: cfgtype=1 not in my guild")
        return
      end
    end
    if (not ksk:UserIsRanked (sender)) then
      debug (2, "BCAST returning: cfgtype=1 not ranked")
      return
    end
  elseif (cfgtype == ksk.CFGTYPE_PUG) then
    -- PUG config
    if (not ksk.inraid) then
      debug (2, "BCAST returning: cfgtype=2 not in raid?")
      return -- Shouldnt ever happen - how do we get it if not in raid?
    end
    if (not ksk.raid or not ksk.raid.players or not ksk.raid.players[sender]) then
      debug (2, "BCAST returning: cfgtype=2 not in players")
      return
    end
    local ix = ksk.raid.players[sender]
    if (not ksk.raid.members or not ksk.raid.members[ix]) then
      debug (2, "BCAST returning: cfgtype=2 not in members")
      return
    end
    if (not ksk.raid.members[ix].ml and not ksk.raid.members[ix].isaorl) then
      debug (2, "BCAST returning: cfgtype=2 not ranked")
      return -- Weird, user should not have been able to press broadcast button
    end
  else
      debug (2, "BCAST returning: cfgtype=%d", cfgtype)
    return
  end

  --
  -- If this is an existing config, and we are an admin, we most probably
  -- want to ignore this. However, if we have not yet started a sync
  -- relationship with anyone (even if we know we are a syncer) we accept
  -- this.
  --
  if (ksk.configs[cfgid]) then
    local myuid = ksk.csdata[cfgid].myuid
    local amadmin = ksk.csdata[cfgid].isadmin
    local senduid = ksk:FindUser (sender, cfgid)
    local sendowner, cksenduid = ksk:IsAdmin (senduid, cfgid)
    if (not cksenduid) then
      cksenduid = senduid
    end
    if (not senduid) then
      -- Otherwise it will have searched for ourselves
      sendowner = nil
    end
    if (sendowner == 2) then
      --
      -- First things first, there are certain key parameters that are
      -- not synced, but rather broadcast (the stuff in the config
      -- admin area). The broadcast will have included all of that
      -- info so we set it here.
      --
      ksk.configs[cfgid].name = ncf.name
      ksk.configs[cfgid].tethered = ncf.tethered
      ksk.configs[cfgid].owner = ncf.owner
    end

    if (amadmin) then
      if (ksk.configs[cfgid].syncing) then
        local remadm = false
        --
        -- We don't actually want to exit immediately here. If the sender
        -- is the config owner, we want to check their list of admins to
        -- see if perhaps we have been removed as an admin. It is the only
        -- way we will ever discover we have been removed if we happened to
        -- be offline when the owner removed us as an admin.
        --

        if (sendowner == 2) then
          --
          -- Yes, it's the owner. Check to make sure we're still an admin.
          -- Actually, we may not even be a user any more, so we will check
          -- that first, and while we're at it, gather the user id.
          --
          local uid
          for k,v in pairs (ncf.users) do
            if (v.name == K.player.player) then
              uid = k
              break
            end
          end
          if (not uid) then
            remadm = true
          else
            if (not ncf.admins[uid]) then
              --
              -- This may be an alt, and our main is the admin. We have to
              -- check that.
              --
              if (string.find (ncf.users[uid].flags, "A") ~= nil) then
                local main = ncf.users[uid].main
                if (not ncf.admins[main] and ncf.owner ~= main) then
                  remadm = true
                end
              else
                remadm = true
              end
            end
          end
        end
        if (not remadm) then
          --
          -- We're safe. We are still an admin and we are syncing, so we
          -- return and ignore this message. If we were told that we are no
          -- longer an admin, if we drop out of this conditional and resume
          -- the normal code path below, it will effectively remove us and
          -- reset the config without us as an admin or user.
          --
          debug (2, "BCAST returning: syncing admin")
          return
        end
      end
    end
  end

  local function handle_cfgdata (hnc, cid)
    if (not ksk.configs[cid]) then
      if (ksk:CreateNewConfig (hnc.name, false, false, true, cid)) then
        return
      end
    end
    local nc = ksk.configs[cid]
    nc.users = nil
    nc.admins = nil
    nc.lists = nil
    nc.syncing = false
    nc.cksum = 0
    nc.lastevent = 0
    nc.nitems = 0
    nc.nusers = 0
    nc.items = {}
    nc.history = {}
    K.CopyTable (hnc, nc)
    ksk:FullRefresh ()
    info (L["configuration %q updated by %q"], white (nc.name), white(sender))
  end

  if (cfgtype == ksk.CFGTYPE_PUG) then
    if (not ksk.configs[cfgid]) then
      ksk:ConfirmationDialog (L["Accept Configuration"],
        strfmt (L["PUGCONFIG"], white (sender), L["MODTITLE"]),
        ncf.name, function (cfgdata) handle_cfgdata (cfgdata, cfgid) end, ncf,
        ksk.mainwin:IsShown (), 220, nil)
      return
    end
  end

  handle_cfgdata (ncf, cfgid)
end

--
-- Command: BIREM itemidx
-- Purpose: Used to remove an item from the list of biddable items. This is
--          sent out to the raid only so that people in the raid tracking the
--          loot process see when an item is manually removed from the list
--          by the ML pressing the "Remove" button. This is also sent whenever
--          an item is awarded to a user and removed off the corpse.
--
ihandlers.BIREM = function (sender, proto, cmd, cfg, ...)
  local itemidx = ...
  if (cfg ~= ksk.currentid) then
    return
  end
  if (not ksk.bossloot) then
    return
  end
  ksk:RemoveItemByIdx (itemidx)
end

--
-- Command: BICAN
-- Purpose: Used to signal a  bid was cancelled.
--
ihandlers.BICAN = function (sender, proto, cmd, cfg, ...)
  if (cfg ~= ksk.currentid) then
    return
  end
  ksk:ResetBidders (false)
end

--
-- Command: FLTCH what val
-- Purpose: Used to change the loot item filter when this is done on the fly
--          by the master looter. This always works on the currently selected
--          loot item so no index is necessary. WHAT tells us which value was
--          changed, and VAL is the new value it is being set to. VAL can
--          sometimes be two or more values depending on what WHAT is set to.
--          WHAT = 'C' - change class filter. VAL is the class ID and the new
--          value (either true or false).
--          WHAT = 'R' - change the role filter to VAL.
--          WHAT = 'G' - change the guild rank filter to VAL.
--          WHAT = 'A' - change the strict class armour filter to VAL
--          WHAT = 'L' - change the strict role filter to VAL
ihandlers.FLTCH = function (sender, proto, cmd, cfg, ...)
  local what, v1, v2, v3 = ...
  if (cfg ~= ksk.currentid) then
    return
  end
  if (not ksk.bossloot) then
    return
  end

  if (what == "C") then
    w = K.IndexClass[v1].w
    ksk.qf.lootrules[w]:SetChecked (v2)
    return
  elseif (what == "R") then
    ksk.qf.lootrules.role:SetValue (v1)
  elseif (what == "G") then
    ksk.qf.lootrules.rank:SetValue (v1)
  elseif (what == "A") then
    ksk.qf.lootrules.strictarmour:SetChecked (v1)
  elseif (what == "L") then
    ksk.qf.lootrules.strictrole:SetChecked (v1)
  end
end

--
-- Command: BIDOP idx timeout
-- Purpose: Opens bidding on the currently selected item. Recipients of this
--          message merely need to activate their "Bid" button in case the
--          user is interested in bidding on the item. This is always preceeded
--          by a BIDCL message to clear the bid list just to be sure.
--          Gets passed the index number of the item being bid on, which is
--          currently ignored as the item will have been set by LISEL. We could
--          do an integrity check though. Also currently unused (for future
--          use) is a timeout which will start a bid timeout countdown on the
--          user's end.
--
ihandlers.BIDOP = function (sender, proto, cmd, cfg, ...)
  local idx, timeout = ...
  if (cfg ~= ksk.currentid) then
    return
  end
  if (not ksk.bossloot) then
    return
  end
  ksk.bidders = {}
  ksk.qf.bidscroll.itemcount = 0
  ksk.qf.bidscroll:UpdateList ()
  ksk.biditem = idx
  ksk.qf.bidders.mybid:SetEnabled (true)
end

--
-- Command: BIDND
-- Purpose: Closes bidding on the current item. The recipients simply need to
--          disable the bid/retract button.
--
ihandlers.BIDND = function (sender, proto, cmd, cfg, ...)
  local idx, timeout = ...
  if (cfg ~= ksk.currentid) then
    return
  end
  if (not ksk.bossloot) then
    return
  end
  ksk.qf.bidders.mybid:SetEnabled (false)
  ksk.qf.bidders.mybid:SetText (L["Bid"])
end

--
-- Command: BIDME idx
-- Purpose: Sent by a user using KSK to the ML when they press the "Bid" button
--          in the UI. IDX is the index of the item being bid on.
--          This is sent by the user using the addon channel rather than
--          via whispers (ie it uses ksk.SendWhisperAM).
--
ihandlers.BIDME = function (sender, proto, cmd, cfg, ...)
  local idx = ...
  if (cfg ~= ksk.currentid) then
    return
  end
  if (not ksk.bossloot) then
    return
  end
  ksk:NewBidder (sender, idx)
end

--
-- Command: BIDRT idx
-- Purpose: Sent by a user using KSK to the ML when they press the "Retract"
--          button in the UI. IDX is the index of the item being big on and is
--          currently unused, as this should always match the current bid
--          item. This is sent by the user using the addon channel rather than
--          via whispers (ie it uses ksk.SendWhisperAM).
--
ihandlers.BIDRT = function (sender, proto, cmd, cfg, ...)
  local idx = ...
  if (cfg ~= ksk.currentid) then
    return
  end
  if (not ksk.bossloot) then
    return
  end
  ksk:RetractBidder (sender, idx)
end

--
-- Command: BIDER name class pos uid prio useprio
-- Purpose: Sent to the raid when a user bids, so that users who have the mod
--          can see who is bidding. NAME is the user who bid on the item and
--          POS is their position within the list being bid on. Note that this
--          will not be sent if silent bidding is enabled. CLASS is the bidders
--          class for class coloring in the list. We do not send out user IDs
--          because the recipients may not have an up to date user list. The
--          UID is sent for completeness sake only and is not used by anyone
--          except the master looter.
--          Added after release 86: PRIO is the assigned priority. The lower
--          the number the higher the priority (1 = highest). Only used for
--          guild configs and only if the option is enabled. A higher priority
--          will always win over a lower one, regardless of list position.
--          USEPRIO is set to true if priorities are being used.
--
ihandlers.BIDER = function (sender, proto, cmd, cfg, ...)
  local name, class, pos, uid, prio, useprio = ...
  if (cfg ~= ksk.currentid) then
    return
  end
  if (not ksk.bossloot) then
    return
  end
  ksk:AddBidder (name, class, pos, uid, prio, useprio, false)
end

--
-- Command: BIDRM name
-- Purpose: Sent to the raid when a user retracts, so that users who have the
--          mod can see who is bidding. NAME is the user who bid on the item.
--
ihandlers.BIDRM = function (sender, proto, cmd, cfg, ...)
  local name = ...
  if (cfg ~= ksk.currentid) then
    return
  end
  if (not ksk.bossloot) then
    return
  end
  ksk:DeleteBidder (name)
end

--
-- Command: MKUSR uid name class refresh
-- Purpose: Sent to the raid / guild when a new user is created. Users who are
--          syncers ignore this as they get their user list via sync messages.
--          NAME is the name of the user being added, CLASS is their class, and
--          REFRESH is set to true if the user list should be refreshed, false
--          if this is part of a loop and we can be expecting a RFUSR command
--          in the near future.
--
ehandlers.MKUSR = function (adm, sender, proto, cmd, cfg, ...)
  local uid, name, class, refresh = ...

  ksk:CreateNewUser (name, class, cfg, getbool (refresh), true, uid, true)
end

--
-- Command: RFUSR raid
-- Purpose: Sent to the raid / guild when the user list needs to be refreshed.
--          This is usually sent after a bulk user add operation has taken
--          place. RAID is set to true if we should refresh the raid as well.
--
ehandlers.RFUSR = function (adm, sender, proto, cmd, cfg, ...)
  local raid = getbool (...)

  ksk:RefreshUsers ()
  if (raid and ksk.inraid and cfg == ksk.currentid) then
    ksk:RefreshRaid ()
  end
end

--
-- Command: RMUSR uid alts
-- Purpose: Sent to the raid / guild when a user is removed from the list.
--          UID is the user ID being removed. ALTS is set to true if we
--          should delete all alts of the user as well.
--
ehandlers.RMUSR = function (adm, sender, proto, cmd, cfg, ...)
  local uid, alts = ...

  if (not adm) then
    if (not ksk.configs[cfg].users[uid]) then
      return
    end
  end

  ksk:DeleteUser (uid, cfg, getbool (alts), true)
end

--
-- Command: MVUSR uid newname
-- Purpose: Sent to the raid / guild when a user is renamed. UID is the ID
--          of the user and NEWNAME is the user's new name.
--
ehandlers.MVUSR = function (adm, sender, proto, cmd, cfg, ...)
  local uid, newname = ...

  if (not adm) then
    if (not ksk.configs[cfg].users[uid]) then
      return
    end
  end

  local euid = ksk:FindUser (newname, cfg)
  if (euid == uid) then
    return
  end
  if (euid) then
    ksk:DeleteUser (euid, cfg, true, true)
  end
  ksk:RenameUser (uid, newname, cfg, true)
end

--
-- Command: RSUSR uid onoff
-- Purpose: Sent to the raid when a user is reserved / unreserved.
--
ihandlers.RSUSR = function (sender, proto, cmd, cfg, ...)
  local uid, onoff = ...

  if (not ksk.configs[cfg].users[uid]) then
    return
  end

  ksk:ReserveUser (uid, onoff, cfg, true)
end

--
-- Command: REQRS
-- Purpose: Sent to the raid when an admin joins the raid group. Other admins
--          in the raid will respond with the list of user ID's they think
--          are reserved.
--
ihandlers.REQRS = function (sender, proto, cmd, cfg, ...)
  if (cfg == "1") then
    return
  end

  if (not ksk.configs[cfg]) then
    return
  end

  if (not ksk.csdata[cfg].isadmin) then
    return
  end

  if (not ksk.csd.reserved) then
    return
  end
  local rus = {}
  for k,v in pairs (ksk.csdata[cfg].reserved) do
    tinsert (rus, k)
  end
  if (#rus ~= 0) then
    ksk.CSendRaidAM (cfg, "ACKRS", "BULK", rus)
  end
end

--
-- Command: ACKRS usertable
-- Purpose: Sent to the raid when an admin responds with the list of users
--          they know to be reserved. USERTABLE is a simple table of
--          user ID's they have set as reserved.
--
ihandlers.ACKRS = function (sender, proto, cmd, cfg, ...)
  local utb = ...

  if (not ksk.configs[cfg]) then
    return
  end

  local senduid = ksk:FindUser (sender, cfg)

  if (not senduid) then
    return
  end

  if (not ksk:IsAdmin (senduid, cfg)) then
    return
  end

  if (not ksk.csdata[cfg].reserved) then
    ksk.csdata[cfg].reserved = {}
  end
  for k,v in pairs (utb) do
    ksk.csdata[cfg].reserved[v] = true
  end
  ksk:RefreshMemberList ()
end

--
-- Command: CHUSR uid flag onoff val
-- Purpose: Sent to the raid / guild when a user attribute is changed. These
--          events are almost never sent, as the normal way of changing these
--          values is through the MDUSR event below, but there are a few
--          corner cases where these may be sent. USERID is the user being
--          changed, FLAGS is the flag being changed, ONOFF is true or false
--          depending on whether the flags is being set or cleared, and VAL
--          is an optional value for the flag for some types ('A' and 'R').
--
ehandlers.CHUSR = function (adm, sender, proto, cmd, cfg, ...)
  local uid, flag, onoff, val = ...

  if (not adm) then
    if (not ksk.configs[cfg].users[uid]) then
      return
    end
  end

  onoff = getbool (onoff)

  if (flag == "E") then
    ksk:SetUserEnchanter (uid, onoff, cfg, true)
  elseif (flag == "F") then
    ksk:SetUserFrozen (uid, onoff, cfg, true)
  elseif (flag == "A") then
    ksk:SetUserIsAlt (uid, onoff, val, cfg, true)
  elseif (flag == "R") then
    ksk:SetUserRole (uid, tonumber(val), cfg, true)
  end

  if (flag ~= "A" and cfg == ksk.currentid) then
    -- SetUserIsAlt does this internally
    if (adm and adm >= 10) then
      ksk:RefreshUsers ()
      ksk:RefreshLists ()
    end
  end
end

--
-- Command: MDUSR uid role ench frozen exempt alt main
-- Purpose: Sent to the raid / guild when a user is modified.
--
ehandlers.MDUSR = function (adm, sender, proto, cmd, cfg, ...)
  local uid, role, ench, frozen, exempt, alt, mainid = ...

  if (not adm) then
    if (not ksk.configs[cfg].users[uid]) then
      return
    end
  end

  ksk:SetUserRole (uid, tonumber(role), cfg, true)
  ksk:SetUserEnchanter (uid, getbool (ench), cfg, true)
  ksk:SetUserFrozen (uid, getbool (frozen), cfg, true)
  ksk:SetUserIsAlt (uid, getbool (alt), mainid, cfg, true)

  if (cfg == ksk.currentid and adm and adm >= 10) then
    ksk:RefreshMemberList ()
  end
end

--
-- Command: MKLST listid name
-- Purpose: Sent to the raid / guild when a new list is created.
--
ehandlers.MKLST = function (adm, sender, proto, cmd, cfg, ...)
  local listid, name = ...

  ksk:CreateNewList (name, cfg, listid, true)
end

--
-- Command: RMLST listid
-- Purpose: Sent to the raid / guild when a list is removed. LISTID is the
--          ID of the list being removed.
--
ehandlers.RMLST = function (adm, sender, proto, cmd, cfg, ...)
  local listid = ...

  if (not adm) then
    if (not ksk.configs[cfg].lists[listid]) then
      return
    end
  end

  ksk:DeleteList (listid, cfg, true)
end

--
-- Command: MVLST listid newname
-- Purpose: Sent to the raid / guild when a list is renamed. LISTID is the ID
--          of the old list and NEWNAME is the list's new name.
--
ehandlers.MVLST = function (adm, sender, proto, cmd, cfg, ...)
  local listid, newname = ...

  if (not adm) then
    if (not ksk.configs[cfg].lists[listid]) then
      return
    end
  end

  local elid = ksk:FindList (newname, cfg)
  if (elid == listid) then
    return
  end
  if (elid) then
    ksk:DeleteList (elid, cfg, true)
  end
  ksk:RenameList (listid, newname, cfg, true)
end

--
-- Command: CPLST listid newid name
-- Purpose: Sent to the raid / guild when a new list is created by copying from
--          and old one. LISTID is the ID of the old list, NEWID is the ID of
--          the new list, and NAME is the name of the new list. If the target
--          list already exists for the user for some reason, we delete it
--          first (but only for non-events).
--
ehandlers.CPLST = function (adm, sender, proto, cmd, cfg, ...)
  local listid, newid, name = ...

  if (not adm) then
    if (ksk.configs[cfg].lists[newid]) then
      ksk:DeleteList (newid, cfg, true)
    end
  end

  ksk:CopyList (listid, name, cfg, newid, true)
end

--
-- Command: CHLST listid sort rank stricta strictr list
-- Purpose: Syncer-only event sent when a list's paramaters are modified.
--          SORT is the sort order,, RANK is the initial guild rank filter,
--          STRICTA is true if strict class armor filtering is in place,
--          STRICTR is true if strict role filtering is in place, LIST is
--          the additional list to suicide on.
--
ehandlers.CHLST = function (adm, sender, proto, cmd, cfg, ...)
  local listid, sortorder, rank, stricta, strictr, slist = ...

  local llist = ksk.configs[cfg].lists[listid]
  if (not llist) then
    return
  end

  llist.sortorder = tonumber (sortorder)
  llist.def_rank = tonumber (rank)
  llist.strictcfilter = getbool (stricta)
  llist.strictrfilter = getbool (strictr)
  if (slist == "0") then
    slist = 0
  end
  llist.extralist = slist

  if (cfg == ksk.currentid) then
    ksk:RefreshLists ()
  end
end

--
-- Command: IMLST uid listid pos
-- Purpose: Sent to the raid / guild when a user is added to a list. UID is the
--          ID of the user being added, LISTID is the ID of the list the user
--          is being added to, and POS is the position within the list.
--
ehandlers.IMLST = function (adm, sender, proto, cmd, cfg, ...)
  local uid, listid, pos = ...

  ksk:InsertMember (uid, listid, tonumber (pos), cfg, true)
end

--
-- Command: SMLST listid ulist
-- Purpose: Sent to the raid / guild when a roll lists member list is set to
--          an explicit user list. This is used by the CSV import function
--          and when configs are copied. LISTID is the list to set the members
--          in, and ULIST is the concatenated list of users, in order.
--
ehandlers.SMLST = function (adm, sender, proto, cmd, cfg, ...)
  local listid, ulist = ...

  ksk:SetMemberList (ulist, listid, cfg, false)
end

--
-- Command: DMLST uid listid
-- Purpose: Sent to the raid / guild when a user is deleted from a list. UID
--          is the ID of the user being deleted, and LISTID is the ID of the
--          list the user is being deleted from.
--
ehandlers.DMLST = function (adm, sender, proto, cmd, cfg, ...)
  local uid, listid = ...

  ksk:DeleteMember (uid, listid, cfg, true)
end

--
-- Command: MMLST uid listid dir
-- Purpose: Sent to the raid / guild when a user is manually moved on a list.
--          UID is the ID of the user being moved, and LISTID is the ID of the
--          list the user is being moved on. DIR specifies the direction of
--          the move, and has the following values: 0 = suicide, 1 = move down
--          one slot, 2 = move up one slot, 3 = move to extreme top of the list
--          (king them). Note that direction 0 (suicide) is only sent when the
--          list is being managed outside of raid. If this was done in raid,
--          then a formal suicide event is sent (as the extreme bottom is not
--          necessarily the bottom of the suicide list when in raid).
--
ehandlers.MMLST = function (adm, sender, proto, cmd, cfg, ...)
  local uid, listid, dir = ...

  ksk:MoveMember (uid, listid, tonumber (dir), cfg)
end

--
-- Command: SULST listid raidlist uid
-- Purpose: Sent to the raid / guild when a user is suicided for either
--          receiving loot or manually being suicided while in a raid.
--          LISTID is the list on which to suicide the user, RAIDLIST is
--          a list of all of the users in the raid / on reserve, and UID is
--          the ID of the user in that list being suicided. The RAIDLIST
--          argument is a single string of all of the user ID's and needs to
--          be split before passing it to the suicide function.
--
ehandlers.SULST = function (adm, sender, proto, cmd, cfg, ...)
  local listid, raidlist, uid = ...
  local raiders = ksk:SplitRaidList (raidlist)

  ksk:SuicideUserLowLevel (listid, raiders, uid, cfg)
end

--
-- Command: MKITM itemid itemlink
-- Purpose: This is a syncer-only event and deals with adding a new item to the
--          item database. ITEMID is the item ID to be added, and ITEMLINK is
--          the full item link.
--
ehandlers.MKITM = function (adm, sender, proto, cmd, cfg, ...)
  local itemid, ilink = ...
  local itemlink = gsub (ilink, "\7", ":")

  ksk:AddItem (itemid, itemlink, cfg, true)
end

--
-- Command: RMITM itemid
-- Purpose: This is a syncer-only event and deals with removing an item from
--          the item database. Users who are not admins do not need or get the
--          item database and therefore do not need this event.
--
ehandlers.RMITM = function (adm, sender, proto, cmd, cfg, itemid)
  ksk:DeleteItem (itemid, cfg, true)
end

--
-- Command: CHITM itemid ign cfilter role list rank nextuser suicide del autodench automl
-- Purpose: This is a syncer-only event and deals with modifying an item in
--          the item database. The item must already exist. IGN is set to
--          Y if the item is to be ignored, N otherwise. If it is ignored
--          none of the other parameters are used. If there is a class
--          specific filter for the item it is passed in CFILTER, otherwise
--          that is the empty string. ROLE is set to the user role or 0 for
--          none. LIST is the list ID of a list to start roles on, or the
--          empty string for none. RANK is the desired initial guild rank,
--          or 999 for none. If the next drop is meant to be assigned to
--          a given user, the user's ID is passed in NEXTUSER, else it is
--          the empty string. If it is not empty, DEL is set to Y if the
--          item is to be deleted after the next assignment, and if the
--          user needs to be suicided on a given list, that list ID is passed
--          in SUICIDE, otherwise its empty.
--
ehandlers.CHITM = function (adm, sender, proto, cmd, cfg, ...)
  local itemid, ign, cfilt, role, lst, rank, nextuser, slist, del, autodench, automl = ...

  local ilist = ksk.configs[cfg].items
  if (not ilist[itemid]) then
    return
  end

  local ii = {ilink = ilist[itemid].ilink }
  if (ign == "Y") then
    ii.ignore = true
  else
    if (cfilt and cfilt ~= "") then
      ii.cfilter = cfilt
    end
    if (role and role ~= "") then
      ii.role = tonumber (role)
    end
    if (lst and lst ~= "") then
      ii.list = lst
    end
    if (rank and rank ~= "") then
      ii.rank = tonumber (rank)
    end
    if (nextuser and nextuser ~= "") then
      ii.user = nextuser

      if (slist and slist ~= "" and slist ~= "0") then
        ii.suicide = slist
      end
      if (del and del == "Y") then
        ii.del = true
      end
    end
    if (autodench and autodench ~= "") then
      ii.autodench = true
    end
    if (automl and automl ~= "") then
      ii.automl = true
    end
  end
  ilist[itemid] = ii

  if (cfg == ksk.currentid and adm and adm >= 10) then
    ksk:RefreshItemList ()
  end
end

--
-- Command: MKADM userid adminid
-- Purpose: Sent when an admin is added to the list of admins. Can only ever
--          be sent by the config owner.
--
ehandlers.MKADM = function (adm, sender, proto, cmd, cfg, ...)
  local uid, adminid = ...

  local cfp = ksk.configs[cfg]

  if (not cfp.admins[uid]) then
    cfp.nadmins = cfp.nadmins + 1
    cfp.admins[uid] = { id = adminid }
  end

  ksk:FullRefresh ()
end

--
-- Command: RMADM userid
-- Purpose: Sent when an admin is removed from the list of admins. Can only
--          ever be sent by the config owner.
--
ehandlers.RMADM = function (adm, sender, proto, cmd, cfg, ...)
  local uid = ...

  local cfp = ksk.configs[cfg]

  --
  -- If I was just removed as an admin, get rid of any sync data I may have
  -- had for other users. Also need to check to see if I am an alt and my
  -- main was just removed as an admin.
  --
  local mymain = nil
  if (ksk:UserIsAlt (ksk.csdata[cfg].myuid, nil, cfg)) then
    mymain = cfp.users[ksk.csdata[cfg].myuid].main
  end
  if (uid == ksk.csdata[cfg].myuid or uid == mymain) then
    cfp.syncing = nil
    cfp.lastevent = 0
    cfp.nitems = 0
    cfp.items = {}
    cfp.cksum = 0
    for k,v in pairs (cfp.admins) do
      v.sync = nil
      v.lastevent = nil
      v.active = nil
    end
  end

  ksk:DeleteAdmin (uid, cfg, true)

  ksk:FullRefresh ()
end

--
-- Command: RSYNC
-- Purpose: This is a syncer-only event and is sent either directly to an
--          individual user via whisper, or to the guild or raid if the
--          "request all" sync button was pressed. Only users who are syncers
--          will respond to the event. Responds with a SYNAK message to the
--          sender, no matter how we receieved it (from the receiver's
--          perspective we cant tell and don't care if we got this as a
--          whisper or a guild/raid broadcast, we respond identically).
--          We keep a table of who sent us sync requests and when, and ignore
--          multiple requests if they come within 10 seconds of each other,
--          in case the sender is spamming the sync request button.
--
local lastrec = {}
ihandlers.RSYNC = function (sender, proto, cmd, cfg, ...)
  if (not ksk.configs[cfg]) then
    return
  end

  local now = GetTime ()
  if (lastrec[sender]) then
    if ((now - lastrec[sender]) < 10) then
      return
    end
  end
  lastrec[sender] = now

  local myuid = ksk.csdata[cfg].myuid
  if (myuid == nil) then
    return
  end

  if (not ksk.csdata[cfg].isadmin) then
    return
  end

  --
  -- I'm an admin in this config. Check to see if the sender is an admin
  -- too. If I don't know they are an admin, it means I am out of date
  -- with respects to the config owner, and I need to sync with them first.
  -- We will issue a warning for this case.
  --
  local theiruid = ksk:FindUser (sender, cfg)
  if (theiruid == nil or not ksk:IsAdmin (theiruid, cfg)) then
    info (L["sync request received from %q but I do not recognise them as an admin. Please sync with the config owner (%q)."], sender, aclass (ksk.configs[cfg].users[ksk.configs[cfg].owner]))
    return
  end

  --
  -- All good. We think we're a syncer, we think they're a syncer, we will
  -- respond. We reply with the highest event number we have, and our CRC.
  -- The person who requested the sync will use this to determine whether
  -- or not they need any events from us. If they do they will send us a
  -- GSYNC message requesting all events from a certain number forward.
  --
  ksk.SendWhisperAM (sender, "SYNAK", "ALERT", ksk.cfg.lastevent, ksk.cfg.cksum)
end

--
-- Command: SYNAK lastevent cksum
-- Purpose: This is a syncer-only event and is sent in reply to us requesting
--          sync info from a user or users. We need to verify that the user
--          really is an admin (as we know things) and reject any replies
--          from users we don't know as admins. We then pass this up to the
--          sync code to determine if we need any events from the user who
--          replied. LASTEVENT is the highest event number the sender has in
--          their logs, and CKSUM is their config checksum.
--
ihandlers.SYNAK = function (sender, proto, cmd, cfg, ...)
  local lastevt, cksum = ...

  if (not ksk.configs[cfg]) then
    return
  end

  --
  -- This should almost never happen (we got a sync reply when we are not
  -- even a known user in the config space). The most likely cause of this
  -- is someone trying to spoof us, so we silently ignore it.
  --
  local myuid = ksk.csdata[cfg].myuid
  if (myuid == nil) then
    return
  end

  if (not ksk.csdata[cfg].isadmin) then
    return
  end

  --
  -- I'm an admin in this config. Check to see if the sender is an admin
  -- too. If I don't know they are an admin, silently ignore the reply.
  --
  local theiruid = ksk:FindUser (sender, cfg)
  local ia, cktheiruid = ksk:IsAdmin (theiruid, cfg)
  if (theiruid == nil or not ia) then
    return
  end

  ksk:ProcessSyncAck (cfg, myuid, theiruid, cktheiruid, lastevt, cksum)
end

--
-- Command: GSYNC lastevent full lasteid cksum
-- Purpose: This is a syncer-only event and is sent by a user requesting
--          sync information from us. LASTEVENT is the last event they
--          have from us (which can be 0). If FULL is true, it means they
--          are requesting a full sync with us, as they do not have any
--          existing sync relationships. This affects the return config
--          we send them by adding in the list of admins we are synced
--          with and the last event we have received from all the admins
--          we know about. If we were not already marked as syncing with
--          other users, we do so now (syncing = true for the config space)
--          because we have just entered into a sync relationship with the
--          sender. Note that in the case of a full sync we actually reply
--          with a FSYNC event. If FULL is false, then this is a normal sync
--          event, and we use LASTEVENT to determine exactly which events
--          we need to send to the user.
--          All events after LASTEVENT are sent as a single large table.
--          LASTEID is the sender's current highest event ID, which can
--          be 0 if this is a first time full sync for them.
--          CKSUM is provided for possible future use and is the senders
--          current config checksum value.
--
ihandlers.GSYNC = function (sender, proto, cmd, cfg, ...)
  local lastevt, full, lasteid, cksum = ...

  if (not ksk.configs[cfg]) then
    return
  end

  local theiruid = ksk:FindUser (sender, cfg)
  local ia, cktheiruid = ksk:IsAdmin (theiruid, cfg)
  if (theiruid == nil or not ia) then
    return
  end

  if (full) then
    info (L["sending sync data to user %s."], aclass (ksk.configs[cfg].users[theiruid]))
    ksk:SendFullSync (cfg, sender, lasteid)
    return
  end

  --
  -- The more usual case, send the person the list of events since the
  -- last one they receieved. Note that this may actually be the first
  -- time we are receiving any data from this user, so they may not be
  -- marked as "active". However, after this operation, they will be.
  -- So we mark them as active. We can be inactive and still have normal
  -- events for them since if they just got a full sync, they could have
  -- picked up from whoever they did it with that they were up to date
  -- up to a certain event ID.
  --
  local mlist = {}
  local them = ksk.configs[cfg].admins[cktheiruid]
  them.active = true
  if (not them.lastevent) then
    them.lastevent = 0
  end
  if (not them.sync) then
    them.sync = { }
  end
  for k,v in pairs (them.sync) do
    local eventid = tonumber (strsub (v, 7, 20))
    if (eventid > lastevt) then
      tinsert (mlist, v)
    end
  end
  info (L["sending sync data to user %s."], aclass (ksk.configs[cfg].users[theiruid]))
  ksk.CSendWhisperAM (cfg, sender, "MSYNC", "ALERT", ksk.csdata[cfg].myuid, theiruid, mlist)
end

--
-- Command: FSYNC cfgdata
-- Purpose: This is a syncer-only event and is sent in response to a GSYNC
--          message above when a full sync is requested. We need to verify
--          that we did in fact request this, and that the sender is who
--          we expect it to be.
--
ihandlers.FSYNC = function (sender, proto, cmd, cfg, ...)
  local cfgd = ...

  if (not ksk.configs[cfg]) then
    return
  end

  local myuid = ksk.csdata[cfg].myuid
  if (myuid == nil) then
    return
  end

  if (not ksk.csdata[cfg].isadmin) then
    return
  end

  --
  -- I'm an admin in this config. Check to see if the sender is an admin
  -- too. If I don't know they are an admin, silently ignore the reply.
  --
  local theiruid = ksk:FindUser (sender, cfg)
  if (theiruid == nil or not ksk:IsAdmin (theiruid, cfg)) then
    return
  end

  local ncf, cfgid = prepare_config_from_bcast (cfgd)

  ncf.syncing = true
  ksk.frdb.configs[cfg] = ncf
  ksk:FullRefresh ()
  --
  -- We will want to clear the list of syncers so that we don't attempt
  -- to get another full sync from someone else.
  --
  ksk:SyncUpdateReplier ()
  ksk:RefreshSyncers (true)
  info (L["sync with user %s complete."], aclass (ncf.users[cfgd.d]))
end

--
-- Command: MSYNC theiruid myuid modlist
-- Purpose: This is a syncer-only event and is sent in response to a GSYNC
--          when they want events after a specific number, and it is not a
--          full sync (which is handled by FSYNC above). THEIRUID is the
--          sender's UID in the config in question, MYUID is my uid in that
--          same config (they calculated it, its a matter of choice that we
--          trust it) and MODLIST is a table with event strings that need to
--          be split and processed.
--
ihandlers.MSYNC = function (sender, proto, cmd, cfg, ...)
  local theiruid, myuid, mlist = ...

  if (not ksk.configs[cfg]) then
    return
  end

  local cp = ksk.configs[cfg]
  local ia, cktheiruid = ksk:IsAdmin (theiruid, cfg)
  if (not cp.users[theiruid] or not ia) then
    return
  end

  if (not cp.users[myuid] or not ksk:IsAdmin (myuid, cfg)) then
    return
  end

  local adm = ksk.csdata[cfg].isadmin

  for k,v in pairs (mlist) do
    local cmd, eid, cks, cstr = strsplit ("\8", v)
    local crc = H:CRC32 (cstr)
    local cks = tonumber (cks)
    if (cks ~= crc) then
      err ("possible hack attempt: mis-matched CRC from %s", white (sender))
      debug (1, "checksum mismatch from %s: 0x%s != 0x%s", sender, K.hexstr (crc), K.hexstr (cks))
      return
    end
    if (not ehandlers[cmd]) then
      debug (1, "unknown sync command %q from %q (p=%d)", cmd, sender, proto)
      return
    end
    local eid = tonumber (eid)
    --
    -- Update our config checksum with the new one and process the event.
    --
    local oldsum = ksk.configs[cfg].cksum
    local newsum = bxor (oldsum, cks)
    ksk.configs[cfg].cksum = newsum
    ksk.configs[cfg].admins[cktheiruid].lastevent = eid
    ehandlers[cmd] (adm, sender, proto, cmd, cfg, strsplit (":", cstr))
  end
  ksk.qf.synctopbar:SetCurrentCRC ()
  cp.admins[cktheiruid].active = true
  if (not cp.admins[cktheiruid].sync) then
    cp.admins[theiruid].sync = {}
  end
  ksk:SyncUpdateReplier (theiruid, ksk.configs[cfg].admins[cktheiruid].lastevent)
  info (L["sync with user %s complete."], aclass (ksk.configs[cfg].users[theiruid]))
  ksk:FullRefresh (true)
end

--
-- Command: CSYNC table
-- Purpose: This is a syncer-only event and is sent to either the guild or
--          the raid and contains a table of all known guild or raid
--          configurations, along with the list of co-admins we are syncing
--          with and their last known event we receieved from them. This is
--          used by the recipient to remove any events prior to that event,
--          as it means the sender already has those events safely saved in
--          their savedvariables folder. This event is only ever sent once
--          by each user when they first log in (or reload their UI).
--
ihandlers.CSYNC = function (sender, proto, cmd, cfg, cfgt)
  for k,v in pairs (cfgt) do
    local cfgid, nadm, astr = strsplit (":", v)
    nadm = tonumber (nadm)
    if (ksk.configs[cfgid]) then
      local cap = ksk.configs[cfgid].admins
      local myuid = ksk.csdata[cfgid].myuid
      local mymainid = ksk.csdata[cfgid].mymainid
      local theiruid = ksk:FindUser (sender, cfgid)
      if (theiruid) then
        if (not cap[theiruid]) then
          local ia, aid = ksk:UserIsAlt (theiruid, nil, cfgid)
          if (ia and cap[aid]) then
            theiruid = aid
          else
            theiruid = nil
          end
        end
      end
      if (myuid and theiruid and cap[theiruid].sync) then
        for i = 1, nadm do
          local admid = strsub (astr, 1, 4)
          local lastid = tonumber (strsub (astr, 5, 18))
          astr = strsub (astr, 19)
          if (admid == myuid or admid == mymainid) then
            local bp = 0
            for kk,vv in ipairs (cap[theiruid].sync) do
              local tid = tonumber (strsub (vv, 7, 20))
              if (tid > lastid) then
                bp = kk
                break
              end
            end
            if (bp > 1) then
              for j = 1, bp - 1 do
                tremove (cap[theiruid].sync, 1)
              end
            end
          end
        end
      end
    end
  end
end

--
-- Command: RCOVR name
-- Purpose: This is a syncer-only event and is sent by a config owner (which
--          is verified by the recipient) when they want to recover their
--          config. We essentially respond with the same data as FSYNC
--          above with a little extra for the full config checksum, history
--          and last known event ID stuff. The sender then processes that
--          reply to rebuild their configuration after some kind of disaster.
--          NAME is the name of the config we want to recover. We cannot use
--          the config ID as the owner may have had to create a whole new
--          config, and the reply will include the config ID, which will
--          update the sender's config ID when they receive the data.
--
ihandlers.RCOVR = function (sender, proto, cmd, cfg, cfgname)
  local cfgid = ksk:FindConfig (cfgname)

  if (not cfgid) then
    return
  end

  local uid = ksk:FindUser (sender, cfgid)
  if (not uid) then
    return
  end

  if (ksk.configs[cfgid].owner ~= uid) then
    return
  end

  --
  -- If I am not an admin (or think I'm not) then ignore.
  --
  if (not ksk.csdata[cfgid].isadmin or ksk.csdata[cfgid].isadmin ~= 1) then
    return
  end

  ksk:SendFullSync (cfgid, sender, nil, true)
end

--
-- Command: RCACK cfgdata
-- Purpose: This is a syncer-only event and is sent by an admin in response
--          to the config owner requesting recovery.
--
ihandlers.RCACK = function (sender, proto, cmd, cfg, cfgdata)
  local cfd, cfdid = prepare_config_from_bcast (cfgdata)
  ksk:RecoverConfig (sender, cfd, cfdid, cfgdata)
end

--
-- Command: OROLL ilink timeout
-- Purpose: Sent when an item is being sent to open roll.
--
ihandlers.OROLL = function (sender, proto, cmd, cfg, ilink, timeout)
  ksk.rolling = 1
  ksk:StartOpenRoll (ilink, timeout)
end

--
-- Command: EROLL
-- Purpose: Sent when a roll ends or is cancelled.
--
ihandlers.EROLL = function (sender, proto, cmd, cfg, ...)
  ksk.rolling = nil
  ksk:EndOpenRoll ()
end

--
-- Command: LHADD timestamp ilink who how
-- Purpose: Sent when a master looter assigns loot through KSK. This is sent
--          to all admins. However, each admin can decide whether or not they
--          are actually recording loot history. If they are not then this
--          event will be silently ignored, otherwise it adds the item to
--          the loot history table for the configuration. TIMESTAMP is when
--          the loot was assigned, ILINK is what loot was assigned, WHO is
--          the userid of who it was assigned to, and HOW is how it was
--          assigned to them ("D" for disenchanted, "R" for won on a roll
--          and a listid if it was rolled for on a list). "B" for BOE
--          assigned to ML, "A" is for auto-assigned.
--
ehandlers.LHADD = function (adm, sender, proto, cmd, cfg, ...)
  local when, what, who, how = ...

  local cfp = ksk.configs[cfg]

  if (not cfp.settings.history) then
    return
  end

  local itemlink = gsub (what, "\7", ":")
  local rf = true
  if (adm and adm >= 10) then
    rf = false
  end
  ksk:AddLootHistory (cfg, when, itemlink, who, how, rf, true)
end

--
-- Command: SUNDO listid movers uid itemlink
-- Purpose: This is a syncer-only event and is sent when the master looter
--          needs to undo a suicide. LISTID is the list on which the undo
--          will take place, MOVERS is the list of all of the users that
--          were affected by the original suicide, in their original order,
--          UID is the ID of the user who received the item and was suicided
--          (which will always be the same thing as movers[1]), and itemlink
--          is the itemlink of the item that was rolled on. That is used to
--          produce the undo record in the loot history list.
--
ehandlers.SUNDO = function (adm, sender, proto, cmd, cfg, ...)
  local listid, movers, uid, ilink = ...

  if (not ksk.configs[cfg]) then
    return
  end

  local rilink = gsub (ilink, "\7", ":")
  local movelist = ksk:SplitRaidList (movers)
  ksk:UndoSuicide (cfg, listid, movelist, uid, ilink, true)
end

